Skip to content

Add IOType system with typed artifact serialization#3118

Open
saeidbarati157 wants to merge 12 commits intoNetflix:masterfrom
saeidbarati157:feat/iotype-system
Open

Add IOType system with typed artifact serialization#3118
saeidbarati157 wants to merge 12 commits intoNetflix:masterfrom
saeidbarati157:feat/iotype-system

Conversation

@saeidbarati157
Copy link
Copy Markdown

@saeidbarati157 saeidbarati157 commented Apr 15, 2026

Summary

On top of #3117, this ships the typed-artifact layer for Metaflow. Scope kept small — the pieces here pull their weight in core; everything else belongs in extensions.

  • IOType ABC — the contract extension authors target.
  • Json and Struct — two concrete types with clear standalone value: wire format for CLI/IPC, cross-language JSON bytes on storage, no pickle code-execution risk. Struct also walks directly-nested @dataclass fields so Outer(inner=Inner(...)) round-trips back to its original type.
  • IOTypeSerializer — the datastore bridge that plugs any IOType into the ArtifactSerializer dispatch from Add pluggable artifact serializer framework #3117, so save/load through the datastore just works (PRIORITY=50, STORAGE-only).

Intentionally not included

  • Primitive wrappers (Int32/Int64/Float32/Float64/Bool/Text). Standard Python numbers and strings flow through PickleSerializer unchanged. Wrapping is opt-in, for cases that need constraints/metadata attached.
  • Tensor. Numpy + byte-order/dtype opinions; belongs in an extension that can own those choices.
  • List / Map / Enum. Thin wrappers whose value over plain JSON is mostly schema emission — not enough on their own for core.
  • Rich Struct.to_spec(). Extensions that ship primitive wrappers can override to emit fully-typed schemas; core just returns {"type": "struct"}.

Design

  • serialize(format=...) / deserialize(data, format=..., **kw) mirror the ArtifactSerializer signature from Add pluggable artifact serializer framework #3117. Same WIRE / STORAGE constants, so a single subclass owns both representations.
    • STORAGE(List[SerializedBlob], metadata_dict) for the datastore save path.
    • WIREstr for CLI args, protobuf payloads, cross-process IPC.
  • Four subclass hooks: _wire_serialize, _wire_deserialize, _storage_serialize, _storage_deserialize.
  • Instantiating without implementing the hooks raises TypeError.
  • IOTypeSerializer handles only STORAGE; wire encoding is produced by calling IOType.serialize(format=WIRE) directly.

Safety

  • Struct._storage_deserialize and IOTypeSerializer.deserialize both require the class named in artifact metadata to be an actual class (isinstance(..., type)) before any further checks. This excludes module-level dataclass instances (which is_dataclass() alone considers valid) and other callables that could be invoked with attacker-controlled kwargs.
  • Importing the metadata-named module can still run module-level side-effect code; the Struct docstring calls this out — don't load artifacts from untrusted sources.

Test plan

  • test_base.py — abstract instantiation, WIRE/STORAGE dispatch, invalid format, equality/hash, spec.
  • test_json_type.py — wire and storage round-trips.
  • test_struct_type.py — dataclass round-trip, dict round-trip, directly-nested dataclass round-trip, container-field pass-through, rejection of non-dataclass and dataclass-instance metadata.
  • test_iotype_serializer.py — bridge can_serialize/can_deserialize, dataclass round-trip, WIRE not supported on the bridge, security (rejects non-IOType classes in metadata).
  • Full unit suite green (excluding pre-existing spin failures).

Saeid Barati and others added 5 commits April 15, 2026 22:02
Introduce ArtifactSerializer ABC with priority-based dispatch, enabling
custom serializers to be plugged in alongside the default pickle path.
This is the foundation for the IOType system which adds typed
serialization for Metaflow artifacts.

New abstractions:
- ArtifactSerializer: base class with can_serialize/can_deserialize/
  serialize/deserialize interface
- SerializerStore: metaclass for auto-registration and deterministic
  priority-ordered dispatch
- SerializationMetadata: namedtuple for artifact metadata routing
- SerializedBlob: supports both new bytes and references to
  already-stored data
- PickleSerializer: universal fallback (PRIORITY=9999) wrapping
  existing pickle logic

No existing code is modified. These are inert until wired into
TaskDataStore in a follow-up commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Register PickleSerializer through the standard Metaflow plugin
mechanism so extensions (e.g., mli-metaflow-custom) can add their
own serializers via ARTIFACT_SERIALIZERS_DESC.

- Add artifact_serializer category to _plugin_categories
- Add ARTIFACT_SERIALIZERS_DESC with PickleSerializer in plugins/__init__.py
- Resolve via ARTIFACT_SERIALIZERS = resolve_plugins("artifact_serializer")

Importing metaflow.plugins now triggers PickleSerializer registration
in SerializerStore, ensuring the registry is populated before
TaskDataStore needs it (commit 3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
save_artifacts now loops through priority-ordered serializers to find
one that can_serialize the object. load_artifacts routes deserialization
through metadata.encoding via can_deserialize. PickleSerializer handles
all existing Python objects as the universal fallback.

Behavioral changes:
- New artifacts get encoding "pickle-v4" (was "gzip+pickle-v4")
- _info[name] gains optional "serializer_info" dict for custom serializers
- Removed hardcoded pickle import from task_datastore.py

Backward compatible:
- Old artifacts with "gzip+pickle-v2" or "gzip+pickle-v4" encoding
  load correctly (PickleSerializer.can_deserialize handles both)
- Missing encoding defaults to "gzip+pickle-v2"
- Missing serializer_info defaults to {}
- _objects[name] stays as single string (multi-blob deferred to PR 1.5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SerializerStore now extends ABCMeta (not type) so @AbstractMethod
  is enforced — incomplete subclasses raise TypeError at definition
- Validate blobs non-empty before accessing blobs[0] in save_artifacts
- Add NOTE on compress_method not yet wired into save path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@romain-intel romain-intel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial comments:

  • the auto registration on types doesn't seem to be included here
  • we have to verify that client will work too (ie: serialization_metadata
  • something about compression (does it work right now)?
  • need to discuss io_types (and that may be in an extension since may be a bit specific).

Comment thread metaflow/io_types/__init__.py
Comment thread metaflow/datastore/artifacts/serializer.py Outdated
Comment thread metaflow/datastore/artifacts/serializer.py Outdated
Comment thread metaflow/datastore/artifacts/serializer.py Outdated
Comment thread metaflow/datastore/task_datastore.py Outdated
Comment thread metaflow/datastore/task_datastore.py Outdated
Comment thread metaflow/datastore/task_datastore.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 83.79888% with 87 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@12a8e08). Learn more about missing BASE report.

Files with missing lines Patch % Lines
metaflow/io_types/tensor_type.py 25.35% 53 Missing ⚠️
metaflow/io_types/struct_type.py 85.89% 7 Missing and 4 partials ⚠️
metaflow/datastore/task_datastore.py 75.00% 4 Missing and 5 partials ⚠️
metaflow/io_types/base.py 89.58% 5 Missing ⚠️
metaflow/io_types/enum_type.py 86.84% 4 Missing and 1 partial ⚠️
metaflow/datastore/artifacts/serializer.py 91.48% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3118   +/-   ##
=========================================
  Coverage          ?   26.79%           
=========================================
  Files             ?      387           
  Lines             ?    51937           
  Branches          ?     9117           
=========================================
  Hits              ?    13919           
  Misses            ?    37223           
  Partials          ?      795           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@saeidbarati157 saeidbarati157 force-pushed the feat/iotype-system branch 5 times, most recently from 603a087 to 15d649a Compare April 18, 2026 01:59
@saeidbarati157 saeidbarati157 marked this pull request as ready for review April 18, 2026 02:09
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR ships the typed-artifact layer for Metaflow: the IOType ABC, Json and Struct concrete types, and IOTypeSerializer as the datastore bridge. All previously-flagged P1 issues have been addressed in this revision — the iotype_module/iotype_class routing-key collision is fixed by placing the authoritative keys after **meta_dict, multi-blob is rejected with a clear error, Struct now uses the _UNSET sentinel correctly, init=False field reconstruction uses object.__setattr__, and both Json and Struct override __hash__ with _make_hashable-based implementations. The one remaining P2 is that the base IOType.__hash__ still propagates TypeError directly for any future extension subclass that wraps an unhashable value without overriding it — a simple try/except identity fallback in the base class would remove this trap for extension authors.

Confidence Score: 5/5

Safe to merge — all previously flagged P1 concerns are resolved, and the only remaining finding is a P2 ergonomics issue for future extension authors.

Every P0/P1 issue from prior review rounds has been addressed: routing-key precedence is correct, multi-blob raises loudly, _UNSET sentinel is consistent, init=False fields reconstruct correctly, and both concrete IOTypes override __hash__ safely. The single open finding (base class __hash__ trap) is P2 and does not block merge.

No files require special attention. metaflow/io_types/base.py has the only P2 nit (base __hash__ fallback).

Important Files Changed

Filename Overview
metaflow/io_types/base.py Defines the IOType ABC, _UNSET sentinel, type registry, and _make_hashable helper. Logic is sound; the only remaining concern is that the base __hash__ is a trap for extension authors who wrap unhashable values without overriding it.
metaflow/io_types/struct_type.py Implements Struct IOType with correct _UNSET sentinel handling, init=False field reconstruction via object.__setattr__, and a safe __hash__ override using _make_hashable. Previously flagged issues are all resolved.
metaflow/io_types/json_type.py Clean Json IOType with correct wire/storage round-trips and a proper __hash__ override that handles _UNSET and uses _make_hashable for dict/list values.
metaflow/plugins/datastores/serializers/iotype_serializer.py Bridge between IOType and the datastore serializer framework. Routing-key precedence is correct (**meta_dict spread before the fixed keys so subclass collisions can't overwrite them). Security guards (isinstance(iotype_cls, type) and issubclass(iotype_cls, IOType)) are in place.
metaflow/datastore/task_datastore.py Multi-blob path now raises an explicit DataException instead of silently discarding blobs[1:]. Single-blob IOTypes are fully supported.
metaflow/datastore/artifacts/lazy_registry.py Lazy import-hook registry for extension serializers. The _WrappedLoader / _SerializerImportInterceptor design is sound; self-removal and re-insertion during find_spec avoids recursion correctly.
metaflow/datastore/artifacts/serializer.py Defines SerializationFormat enum (subclasses str for backward compatibility), SerializationMetadata, SerializedBlob, SerializerStore metaclass, and ArtifactSerializer ABC. All correct.
metaflow/plugins/datastores/serializers/pickle_serializer.py Universal fallback serializer using pickle v4. Correctly rejects WIRE format and acts as the last-resort catch-all at PRIORITY=9999.
test/unit/io_types/test_struct_type.py Comprehensive test coverage including init=False fields (mutable and frozen), security rejection of non-dataclass and dataclass-instance metadata, _UNSET sentinel distinction, and hashability with mutable fields.
test/unit/io_types/test_iotype_serializer.py Tests bridge can_serialize/can_deserialize, round-trips for Json and Struct, WIRE rejection, security rejection of non-IOType classes, and verifies that subclass metadata cannot overwrite routing keys.
test/unit/io_types/test_base.py Contract tests for the IOType ABC covering wire/storage dispatch, descriptor behaviour, equality/hash, and invalid-format rejection.
test/unit/io_types/test_json_type.py Good coverage of wire and storage round-trips, numeric-equivalence hash contract, and in-set deduplication.
test/unit/test_artifact_serializer.py Tests SerializerStore registration, ordering, SerializedBlob, and wire/storage dispatch. Module-scoped fixture correctly restores the global registry after the module runs.

Reviews (5): Last reviewed commit: "Harden P1 fixes: frozen dataclasses + nu..." | Re-trigger Greptile

Comment thread metaflow/plugins/datastores/serializers/iotype_serializer.py
Comment thread metaflow/io_types/struct_type.py Outdated
Wire vs storage format
- Added WIRE and STORAGE constants in metaflow.datastore.artifacts.serializer.
- ArtifactSerializer.serialize/deserialize now accept a ``format`` kwarg so a
  single class can own both the storage path (datastore blobs + metadata) and
  the wire path (string for CLI args, protobuf payloads, cross-process IPC).
- PickleSerializer implements STORAGE only; WIRE raises NotImplementedError
  with an explanation (pickle bytes are not safe as a wire payload).
- Serializers that want wire support implement it on the same class; there's
  no need for a second class per format.

Lazy import-hook registry
- New module metaflow/datastore/artifacts/lazy_registry.py with
  SerializerConfig, an importlib.abc.MetaPathFinder interceptor, and public
  register_serializer_for_type / load_serializer_class entry points.
- If the target type's module is already in sys.modules, registration is
  immediate. Otherwise a hook is installed on sys.meta_path and registration
  fires the first time the user's code imports the target module. This
  defers the cost of serializer-module imports (torch, pyarrow, fastavro,
  ...) until those dependencies are actually in play.
- find_spec temporarily removes the interceptor from sys.meta_path during
  its lookup to avoid recursion.

Review nits
- serializer.py: rename SerializationMetadata.type field to obj_type to avoid
  shadowing the type() builtin (dict key inside _info stays "type" for
  backward compatibility with existing datastores).
- serializer.py: SerializerStore skips TYPE=None AND any subclass that is
  still abstract, via inspect.isabstract().
- serializer.py: get_ordered_serializers memoizes the sorted list and
  invalidates on new registration. Drops the O(n²) list.index tiebreaker —
  Python 3.7+ dicts preserve insertion order.
- task_datastore.py: lift SerializerStore / SerializationMetadata imports to
  module top (were on hot paths: __init__ and load_artifacts).
- task_datastore.py: the "no serializer claimed this artifact" branch now
  raises DataException with a message pointing at the PickleSerializer
  fallback invariant, instead of the misleading UnpicklableArtifactException.
- task_datastore.py: "no deserializer claimed this artifact" now looks up
  ``serializer_info["source"]`` and hints at a missing extension.
- SerializedBlob.compress_method: removed. It was documented as "not wired
  into the save path" — shipping an unwired knob invites extension authors
  to rely on it and discover it is a no-op. Will come back with its
  consuming code in a later change.
- serialize() docstring rewritten: the side-effect-free contract is there
  to support retries, caching, and parallel dispatch, and to keep
  serializers testable; I/O belongs in hooks.

Tests
- New test/unit/test_lazy_serializer_registry.py (9 tests covering config
  validation, eager registration for already-imported types, deferred
  registration through the import hook, and the recursion guard).
- test_artifact_serializer.py gains format-dispatch coverage (STORAGE and
  WIRE round-trip on a toy dual-format serializer; PickleSerializer raises
  on WIRE).
- Removed tests for compress_method and adjusted tests that poked at
  _registration_order so they go through the public API only.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread metaflow/io_types/struct_type.py
Saeid Barati and others added 6 commits April 23, 2026 22:57
- Add type annotations to ArtifactSerializer method signatures for better
  editor/IDE support.
- Remove unused ``context`` parameter from ``deserialize`` (call sites
  never passed a meaningful value).
- Promote ``STORAGE`` / ``WIRE`` to a ``SerializationFormat`` enum;
  subclass ``str`` so the existing string-literal comparisons keep working.
- Reword the ``_serializers_override`` comment and move the property
  rationale next to the property definition.
- Extend the multi-blob error message with "at this time. If you have a
  need for multi blob serializers, please reach out to the Metaflow
  team.".
- Include ``serializer_info`` in the "No deserializer claimed artifact"
  error message.
On top of Netflix#3117, this ships the typed-artifact layer. Scope kept
deliberately small:

- ``IOType`` ABC — the contract extension authors target.
- ``Json`` and ``Struct`` — two concrete types with clear standalone
  value in core: wire format for CLI/IPC, cross-language JSON bytes
  on storage, no pickle code-execution risk. ``Struct`` also walks
  directly-nested ``@dataclass`` fields so ``Outer(inner=Inner(...))``
  round-trips back to its original type (generic containers like
  ``List[Inner]`` come back as raw JSON values — wrap those
  explicitly when you need richer reconstruction).
- ``IOTypeSerializer`` — the bridge that plugs any ``IOType`` instance
  into the ``ArtifactSerializer`` dispatch added by Netflix#3117 so save/load
  through the datastore just works.

What's intentionally *not* in this PR

- Primitive wrappers (Int32/Int64/Float32/Float64/Bool/Text). Standard
  Python numbers and strings flow through ``PickleSerializer``
  unchanged. Wrapping is opt-in, for cases where you want
  constraints/metadata attached.
- ``Tensor``. Pulls in numpy + byte-order/dtype opinions; belongs in an
  extension that can own those choices.
- ``List`` / ``Map`` / ``Enum``. Thin wrappers whose value over plain
  JSON is mostly schema emission — not enough on their own for core.
- Rich schema emission from ``Struct.to_spec()``. Extensions that ship
  primitive wrappers can override to emit fully-typed schemas; core
  just returns ``{"type": "struct"}``.

Contract

``serialize(format=...)`` / ``deserialize(data, format=..., **kw)``
mirror the ``ArtifactSerializer`` signature from Netflix#3117 and use the same
``WIRE`` / ``STORAGE`` constants, so one subclass owns both
representations:

- ``STORAGE`` → ``(List[SerializedBlob], metadata_dict)`` for
  persisting through the datastore.
- ``WIRE`` → ``str`` for CLI args, protobuf payloads, and
  cross-process IPC.

Subclasses implement four hooks (``_wire_serialize``,
``_wire_deserialize``, ``_storage_serialize``,
``_storage_deserialize``). Instantiating without the hooks raises
``TypeError``.

``IOTypeSerializer`` is registered via ``ARTIFACT_SERIALIZERS_DESC``
with ``PRIORITY=50`` — ahead of the default 100 so it catches
``IOType`` instances before a generic catch-all, and always ahead of
the ``PickleSerializer`` fallback (9999). It implements only
``STORAGE``; wire encoding is produced by calling
``IOType.serialize(format=WIRE)`` directly.

Safety

- ``Struct._storage_deserialize`` and ``IOTypeSerializer.deserialize``
  both require the class named in artifact metadata to be an actual
  class (``isinstance(..., type)``) before any further checks. This
  excludes module-level dataclass *instances* (``is_dataclass`` alone
  returns ``True`` for those) and other callables that could be
  invoked with attacker-controlled kwargs.
- Importing the metadata-named module can still run module-level
  side-effect code; the ``Struct`` docstring calls this out so callers
  don't load artifacts from untrusted sources.

Tests

- ``test_base.py`` — abstract instantiation, WIRE/STORAGE dispatch,
  invalid format, equality/hash, spec.
- ``test_json_type.py`` — wire and storage round-trips.
- ``test_struct_type.py`` — dataclass round-trip, dict round-trip,
  directly-nested dataclass round-trip, container-field pass-through,
  rejection of non-dataclass and dataclass-instance metadata.
- ``test_iotype_serializer.py`` — bridge
  ``can_serialize``/``can_deserialize``, round-trip through dataclass
  reconstruction, rejection of non-IOType classes in metadata, WIRE
  not supported on the bridge.
The metadata service stores only the artifact encoding string
("iotype:<type_name>"), not the full serializer_info dict. So when the
Flow client reconstructs an artifact, IOTypeSerializer.deserialize had no
iotype_module/iotype_class to import and KeyError'd.

Populate a registry via IOType.__init_subclass__ keyed by type_name, and
have IOTypeSerializer.deserialize resolve the class from the encoding
suffix first. iotype_module/iotype_class hints remain a secondary lookup
for introspecting artifacts whose owning extension isn't loaded locally.

Fixes round-trip of IOType artifacts through Flow(...).latest_run
.end_task[...].data without touching the metadata service or client
core.py.
``dataclasses.fields()`` includes fields declared with ``field(init=False,
...)``, but those are not accepted by the generated ``__init__``. Passing
them as kwargs raised ``TypeError: __init__() got an unexpected keyword
argument``. ``dataclasses.asdict`` does include them in its output, so the
storage blob carried them.

Fix: partition fields into init-eligible kwargs and post-construction
``setattr`` fields. The serialized value wins over ``__post_init__``'s
recomputation or a field default.

Adds a regression test that mutates the init=False field after
construction and verifies the mutated value survives a storage
round-trip.
The base ``IOType.__hash__`` does ``hash((type(self), self._value))``, which
raises ``TypeError`` when ``_value`` is inherently unhashable — a dict
wrapped by ``Json({...})`` or a dataclass with mutable fields (``list``,
``dict``) wrapped by ``Struct(...)``. Both are the primary ways users
create instances of these types, so the wrapper ends up mis-behaving in
any set/dict or cache keyed by artifacts.

Override ``__hash__`` on both subclasses to hash the canonical JSON
representation (already produced by the wire/storage path). This is:

- Stable — ``sort_keys=True`` makes the JSON deterministic across equal
  dicts/dataclasses regardless of key insertion order.
- Contract-safe — ``__eq__`` compares raw values; equal values flatten to
  identical sorted-key JSON, so ``a == b`` implies ``hash(a) == hash(b)``.
- Descriptor-aware — when ``_value is _UNSET`` (no value, pure type
  descriptor), fall back to hashing the sentinel directly.

Adds regression tests covering ``Json({...})``, ``Json([...])``,
``Struct(dc_with_list)``, plain ``Struct({...})``, and the ``Struct()``
descriptor.
Self-review before pushing caught two subtle regressions in the prior two
commits:

1. ``_reconstruct`` used plain ``setattr`` to restore ``init=False`` fields
   after construction, which raises ``FrozenInstanceError`` on
   ``@dataclass(frozen=True)``. Switch to ``object.__setattr__`` — the same
   bypass frozen dataclasses themselves use in ``__post_init__``.

2. ``Json.__hash__`` / ``Struct.__hash__`` hashed the canonical JSON string.
   That renders ``1`` and ``1.0`` as distinct strings, violating the
   ``__eq__`` / ``__hash__`` contract when users mix numeric types
   (``1 == 1.0 == True`` in Python, but ``hash('1') != hash('1.0')``).
   Replace with a recursive ``_make_hashable`` that converts dicts to
   ``frozenset`` of items and lists to ``tuple``s. Python's built-in
   hashing then collapses ``1 == 1.0 == True`` to the same bucket, and
   equal wrapped values produce equal hashes.

``_make_hashable`` lives in ``base.py`` so any future IOType wrapping
unhashable values can reuse it.

Regression tests added for:
- Frozen dataclass with ``init=False`` field round-trip.
- ``Json({'x': 1}) == Json({'x': 1.0})`` and ``hash()`` equality.
- Same for ``Struct({'x': 1})`` / ``Struct({'x': 1.0})``.
- ``Json({'x': True}) == Json({'x': 1})`` hash equality.
- ``Json([1, 2, 3]) == Json([1.0, 2.0, 3.0])`` hash equality.
@saeidbarati157
Copy link
Copy Markdown
Author

Latest push (4e1dfb8, rebased on the updated #3117 tip fed2c78):

Addresses both P1s Greptile flagged:

  • Struct._reconstruct now filters init=False fields out of the constructor kwargs and restores them via object.__setattr__ — frozen-dataclass safe.
  • Json.__hash__ / Struct.__hash__ override the base to use a new _make_hashable helper (recursive frozenset/tuple conversion). Preserves Python's numeric equivalence, so Json({'x': 1}) == Json({'x': 1.0}) and their hashes match.

Other cleanup folded into the rebase:

Stale review threads resolved — the 6 datastore/* comments point at files whose state is now fully managed by #3117 (type hints, SerializationFormat enum, clearer error messages, _serializers_override). Happy to re-open any of them if you'd like a separate discussion.

Greptile re-review: 5/5 (up from 3/5). 120/120 io_types + pluggable-serializer unit tests pass.

Optional P2 remaining (per Greptile summary): the base IOType.__hash__ still raises TypeError if a future extension subclass wraps an unhashable value without overriding. Can add a try/except identity fallback there if useful — not blocking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants